Цели исследования:
Задачи:
Проанализировать связь целевого события - просмотра контактов - и других действий пользователей:
Оценить, какие действия чаще всего совершают те пользователи, которые просматривают контакты:
Проверить статистические гипотезы:
Ход работы:
Подключим необходимые для работы библиотеки.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
import numpy as np
import requests
from tqdm.auto import tqdm
import math as mth
from scipy import stats as st
Считаем данные о пользователях приложения по перепродаже вещей из файлов и выведем их содержимое на экран. Сразу при считывании файла преобразуем формат даты и времени в формат datetime.
data = pd.read_csv('dataset/mobile_dataset.csv', parse_dates=['event.time'])
sources = pd.read_csv('dataset/mobile_sources.csv')
display(data.head(10).style.set_caption("Содержимое датафрейма mobile_dataset.csv"))
data.info()
| event.time | event.name | user.id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 5 | 2019-10-07 00:01:19.993624 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 6 | 2019-10-07 00:01:27.770232 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 7 | 2019-10-07 00:01:34.804591 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 8 | 2019-10-07 00:01:49.732803 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 9 | 2019-10-07 00:01:54.958298 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event.time 74197 non-null datetime64[ns] 1 event.name 74197 non-null object 2 user.id 74197 non-null object dtypes: datetime64[ns](1), object(2) memory usage: 1.7+ MB
display(sources.head(10).style.set_caption("Содержимое датафрейма mobile_sources.csv"))
sources.info()
| userId | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 | |
| 5 | 17f6b2db-2964-4d11-89d8-7e38d2cb4750 | yandex |
| 6 | 62aa104f-592d-4ccb-8226-2ba0e719ded5 | yandex |
| 7 | 57321726-5d66-4d51-84f4-c797c35dcf2b | |
| 8 | c2cf55c0-95f7-4269-896c-931d14deaab5 | |
| 9 | 48e614d6-fe03-40f7-bf9e-4c4f61c19f64 | yandex |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4293 entries, 0 to 4292 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 userId 4293 non-null object 1 source 4293 non-null object dtypes: object(2) memory usage: 67.2+ KB
При первичном изучении данных, заметим, что:
data - 74197, а для таблицы sources - 4293;datetime, в который занесем дату события event.time, также для корректной работы изменим тип столбца event.time на тип datetime, сейчас тип столбца - object;Приведем названия столбцов к соответствующему виду.
#Переименуем столбцы
data = data.rename(columns={'event.time': 'event_time', 'event.name': 'event_name', 'user.id': 'user_id'})
sources = sources.rename(columns={'userId': 'user_id'})
display(data.head(), sources.head())
| event_time | event_name | user_id | |
|---|---|---|---|
| 0 | 2019-10-07 00:00:00.431357 | advert_open | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 1 | 2019-10-07 00:00:01.236320 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 2 | 2019-10-07 00:00:02.245341 | tips_show | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| 3 | 2019-10-07 00:00:07.039334 | tips_show | 020292ab-89bc-4156-9acf-68bc2783f894 |
| 4 | 2019-10-07 00:00:56.319813 | advert_open | cf7eda61-9349-469f-ac27-e5b6f5ec475c |
| user_id | source | |
|---|---|---|
| 0 | 020292ab-89bc-4156-9acf-68bc2783f894 | other |
| 1 | cf7eda61-9349-469f-ac27-e5b6f5ec475c | yandex |
| 2 | 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 | yandex |
| 3 | d9b06b47-0f36-419b-bbb0-3533e582a6cb | other |
| 4 | f32e1e2a-3027-4693-b793-b7b3ff274439 |
Названия столбцов изменились в двух таблицах. Теперь изменим тип столбца с датой и временем события и создадим дополнительно столбец с отдельной датой события.
data['date'] = pd.to_datetime(data['event_time'].dt.date)
display(data.sample(5))
data.info()
| event_time | event_name | user_id | date | |
|---|---|---|---|---|
| 16980 | 2019-10-14 14:17:58.445899 | photos_show | 6383ff6a-04b8-4562-a98f-bb4f760d3c39 | 2019-10-14 |
| 60810 | 2019-10-29 21:49:30.603863 | favorites_add | cab083cd-103f-4181-8f1d-362792e6d058 | 2019-10-29 |
| 50015 | 2019-10-26 15:15:01.695025 | tips_show | 9cb149ed-5424-4c3c-82cd-4033f8ab7468 | 2019-10-26 |
| 70886 | 2019-11-02 20:19:31.963779 | tips_click | dd61b125-4b5f-4312-82dc-dcc431980265 | 2019-11-02 |
| 6605 | 2019-10-09 18:57:56.458239 | tips_show | da1c9773-59cb-43e4-a853-18221d924588 | 2019-10-09 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 74197 entries, 0 to 74196 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_time 74197 non-null datetime64[ns] 1 event_name 74197 non-null object 2 user_id 74197 non-null object 3 date 74197 non-null datetime64[ns] dtypes: datetime64[ns](2), object(2) memory usage: 2.3+ MB
Добавили новый столбец с датой совершения действия, изменили типо в столбце с датой и временем события, теперь тип данных этих двух столбцов - datetime64.
В таблице также имеются два события, которые обозначают одно и то же действие. Проверим каких из них больше по количеству и приведем события к одному виду для удобства и корректных расчетов в дальнейшем.
len(data.query('event_name == "contacts_show"'))
4450
len(data.query('event_name == "show_contacts"'))
79
Количество действий show_contacts значительно меньше, чем contacts_show. Поэтому заменим действие show_contacts на contacts_show.
data['event_name'] = data['event_name'].replace('show_contacts', 'contacts_show')
len(data.query('event_name == "contacts_show"'))
4529
len(data.query('event_name == "show_contacts"'))
0
data['event_name'].nunique()
15
Заменили событие show_contacts на contacts_show. Проверили, что количество уникальных событий сократилось с 16 до 15 и в столбце event_name не осталось событий show_contacts.
Проверим количество пропущенных значений в столбцах.
display(data.isna().sum(), sources.isna().sum())
event_time 0 event_name 0 user_id 0 date 0 dtype: int64
user_id 0 source 0 dtype: int64
Пропуски в данных в двух таблицах отсутствуют. Проверим, есть ли в таблице полные дубликаты.
display(data.duplicated().sum(), sources.duplicated().sum())
0
0
Проверим неявные дубликаты.
dupl_data = data[data.duplicated(subset=['user_id', 'event_time'])]
dupl_data
| event_time | event_name | user_id | date |
|---|
Полные и неявные дубликаты в таблицах отсутствуют.
Посчитаем количество уникальных пользователей и сравних совпадает ли число уникальных пользователей в двух таблицах.
data['user_id'].nunique()
4293
sources['user_id'].nunique()
4293
Количество уникальных пользователей в таблицах совпадает.
Вывод:
event_time тип данных был заменен на datetime, был создан дополнительный столбез с датой события date типа datetime;event_name была произведена замена события show_contacts на contacts_show для корректных расчетов в дальнейшем, так как данные события отвечают за одно и то же действие.Изучим за какой период представлены данные в таблице.
print('Минимальная дата:', data['date'].min(), '\nМаксимальная дата', data['date'].max())
Минимальная дата: 2019-10-07 00:00:00 Максимальная дата 2019-11-03 00:00:00
В таблице представлены данные с 7 октября 2019 года до 3 ноября 2019 года. Посмотрим как распределены данные по этому периоду. В какое время пользователи чаще пользуются приложением, а в какое реже.
plt.title('Распределение данных по дате и времени')
plt.xlabel('Дата')
plt.ylabel('Активность пользователей')
data['event_time'].hist(bins=100, figsize=(20, 5), ec="blue", fc="green", alpha=0.6, linewidth=2)
plt.show()
plt.title('Распределение данных по времени суток')
plt.xlabel('Время суток')
plt.ylabel('Активность пользователей')
data['event_time'].dt.hour.hist(bins=24, figsize=(20, 5),\
ec="blue", fc="green", alpha=0.6, linewidth=2)
plt.xticks(range(0, 23))
plt.show()
На первом графике видим, что в дневное время активность пользователей выше, а в ночное меньше, за счет чего можем сказать что данные по дням распределены нормально. При этом активность пользователей каждый день примерно одинаковая. Нет таких периодов, когда в какой-то из дней активности не было совсем или активность все сутки была равномерной. Для анализа можем использовать данные за весь период.
На втором графике распределения активности по часам в сутках можем заметить, что наибольшая активность пользователей в приложении приходится на обеденное время, в период с 14 до 16 часов дня, а так же высокая активность пользователей вечером, около 20-21 вечера. Менее всего пользователи пользуются приложением в период с 3 ночи до 6 утра.
Проверим, сколько всего событий представлено в таблице, количество уникальных пользователей и сколько в среднем приходится событий на пользователя.
print('Всего событий:', data['event_name'].shape[0])
print('Всего уникальных видов событий:', data['event_name'].nunique())
print('Всего уникальных пользователей:', data['user_id'].nunique())
print('В среднем на пользователя приходится:', round(data.groupby('user_id')['event_name'].count().median()))
Всего событий: 74197 Всего уникальных видов событий: 15 Всего уникальных пользователей: 4293 В среднем на пользователя приходится: 9
Посмотрим более наглядно на то, сколько событий совершают пользователи.
event_by_user = data.groupby('user_id')['event_name'].count().reset_index()
event_by_user.columns = ['user_id', 'event_count']
print(round(event_by_user['event_count'].describe(), 2))
#Строим график распределения количества событий на пользователя
sns.displot(event_by_user['event_count'], bins=100, kde=True, height=7, )
plt.title('Распределение количества событий на пользователя')
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.grid(which='major')
plt.show()
count 4293.00 mean 17.28 std 29.13 min 1.00 25% 5.00 50% 9.00 75% 17.00 max 478.00 Name: event_count, dtype: float64
Как видим, в среднем пользователь совершает около 9 событий. В данном случае медианное значение более приближено к реальности, так как у нас присутствуют выбросы в виде крупных значений: некоторые пользователи совершают практически по 478 событий.
Теперь посмотрим какие события совершают пользователи чаще, а какие реже. Найдем количество уникальных пользователей, которые совершают каждое из действий и найдем долю, которая покажет, какой процент уникальных пользователей совершил то или иное событие.
events = data.groupby('event_name')['user_id'].count().reset_index().sort_values(by='user_id', ascending=False)
users = data.groupby('event_name')['user_id'].nunique().reset_index().sort_values(by='user_id', ascending=False)
users['part'] = round(users['user_id'] / data['user_id'].nunique() * 100, 2)
events = events.merge(users, on='event_name')
events.columns = ['Наименование события', 'Кол-во событий', 'Кол-во пользователей', 'Доля пользователей']
cm = sns.light_palette("lightblue", as_cmap=True)
display(events.style.background_gradient(cmap=cm)\
.set_caption('Таблица с частотой встречаемости событий и долей уникальных пользователей, совершивших это событие'))
#Построим график показывающий частоту встречаемости событий
plt.figure(figsize=(17, 6))
sns.barplot(data=events, x='Кол-во событий', y='Наименование события', ec="blue", fc="green", alpha=0.6)
for i, v in enumerate(events['Кол-во событий']):
plt.text(v+3000, i, str(round(v, 2)), ha = 'center', size = 12)
plt.title('Частота событий', fontsize=14)
plt.ylabel('Наименование событий', fontsize=14)
plt.xlabel('Частота встречаемости события', fontsize=14)
plt.show()
| Наименование события | Кол-во событий | Кол-во пользователей | Доля пользователей | |
|---|---|---|---|---|
| 0 | tips_show | 40055 | 2801 | 65.250000 |
| 1 | photos_show | 10012 | 1095 | 25.510000 |
| 2 | advert_open | 6164 | 751 | 17.490000 |
| 3 | contacts_show | 4529 | 981 | 22.850000 |
| 4 | map | 3881 | 1456 | 33.920000 |
| 5 | search_1 | 3506 | 787 | 18.330000 |
| 6 | favorites_add | 1417 | 351 | 8.180000 |
| 7 | search_5 | 1049 | 663 | 15.440000 |
| 8 | tips_click | 814 | 322 | 7.500000 |
| 9 | search_4 | 701 | 474 | 11.040000 |
| 10 | contacts_call | 541 | 213 | 4.960000 |
| 11 | search_3 | 522 | 208 | 4.850000 |
| 12 | search_6 | 460 | 330 | 7.690000 |
| 13 | search_2 | 324 | 242 | 5.640000 |
| 14 | search_7 | 222 | 157 | 3.660000 |
Событие, которое совершают пользователи чаще всего - tips_show (увидел рекомендованное объявление). Если смотреть по количеству уникальных пользователей, которые совершили каждое из событий, то наибольшая доля приходится на событие tips_show, а также на событие map и photos_show. Именно эти три события совершает больший процент уникальных пользователей приложения.
Для нахождения длительности сессий пользователя необходимо будет отсортировать данные датафрейма по возрастанию по пользователям и времени совершения действий. Потом найти время тайм-аутов, когда пользователь был не активен в приложении, и после сможем уже вычислить длительность сессии.
#Отсортируем таблицу по пользователю и времени совершения события
data = data.sort_values(by=['user_id', 'event_time'], ascending=True)
data.head()
| event_time | event_name | user_id | date | |
|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 |
Теперь можем найти время тайм-аутов можду сессиями пользователей, так как таблица отсортирована по возрастанию по времени и пользователям, то можем найти разницу во времени между совершением действий.
#Создадим датафрейм с тайм-аутами
timeout = pd.DataFrame()
timeout = data.groupby('user_id')['event_time'].diff().reset_index()
timeout = timeout.drop(columns='index')
#Добавим столбец с длиной тайм-аута в минутах
timeout['event_time_min'] = timeout['event_time'].dt.total_seconds()/60
timeout.head()
| event_time | event_time_min | |
|---|---|---|
| 0 | NaT | NaN |
| 1 | 0 days 00:00:45.063550 | 0.751059 |
| 2 | 0 days 00:00:34.669580 | 0.577826 |
| 3 | 0 days 00:02:15.012972 | 2.250216 |
| 4 | 0 days 00:02:10.182041 | 2.169701 |
Создали таблицу с тайм-аутами пользователей, также добавили в нее столбец с длительностью тайм-аута в минутах. Построим диаграмму размаха, чтобы определить какое время тайм-аута нам необходимо брать для нахождения длительности сессий.
#Построим диаграмму размаха
sns.boxplot(x=timeout['event_time_min'])
plt.title('Диаграмма размаха длительностей тайм-аута')
plt.xlabel('Длина тайм-аута в минутах')
plt.show()
В таблице есть слишком длинные тайм-ауды, отбросим их, чтобы посмотреть, где расположены 1 и 3 квартили и медиана.
sns.boxplot(x=timeout['event_time_min'],
showfliers=False)
plt.title('Диаграмма размаха длительностей тайм-аута')
plt.xlabel('Длина тайм-аута в минутах')
plt.show()
Можем заметить, что большинство значений тайм-аута находится в пределах 7 минут.
print('Длительность таймаута по медиане: {} минуту'.format(round(timeout['event_time_min'].quantile(0.5))))
Длительность таймаута по медиане: 1 минуту
Построим график распределения длительности тайм-аутов пользователей.
timeout = timeout.query('event_time_min < event_time_min.quantile(0.9)')
sns.displot(timeout['event_time_min'], bins=100, kde=True, height=7, )
plt.title('Распределение длительности тайм-аута пользователя')
plt.xlabel('Длительность тайм-аута в минутах')
plt.ylabel('Частота встречаемости')
plt.grid(which='major')
plt.show()
Исходя из вышепредставленных графиков и вычислений среднее значение продолжительности тайм-аута равно 1 минуте, а продолжительность тайм-аута по квантилю от 90% равна 7 минутам. Возьмем значение 7 минут тайм-аута для последующих вычислений. Так как большое количество коротких тайм-аутов может говорить о том, что пользователи еще совершают действия в пределах одной сессии. Внутри сессии короткие тайм-ауты, а между сессий тайм-ауты длиннее. После 7 минут же количество тайм-аутов снижается и выравнивается.
#Делим действия пользователей по сессиям с промежутком в 7 минут
g = (data.groupby('user_id')['event_time'].diff() > pd.Timedelta('7Min')).cumsum()
#Добавим новый столбец с номером сессии
data['session_id'] = data.groupby(['user_id', g], sort=False).ngroup() + 1
data.head().style.set_caption('Таблица пользователей с номерами сессий')
| event_time | event_name | user_id | date | session_id | |
|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 00:00:00 | 1 |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 00:00:00 | 1 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 00:00:00 | 1 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 00:00:00 | 1 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 00:00:00 | 1 |
Найдем длительность сессии пользователей. Для этого сгруппируем датафрейм по сессиям и времени, после чего из времени последнего действия в сессии отнимаем время первого действия в этой же сессии и таким образом находим разницу - длительность сессии.
data['session_duration'] = data[data['session_id'].notnull()]\
.groupby('session_id')['event_time']\
.transform(lambda x: x.iat[-1] - x.iat[0])
data['session_duration'] = round(data['session_duration'].dt.total_seconds())
data.head()
| event_time | event_name | user_id | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
| 806 | 2019-10-07 13:40:31.052909 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
| 809 | 2019-10-07 13:41:05.722489 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
| 820 | 2019-10-07 13:43:20.735461 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
| 830 | 2019-10-07 13:45:30.917502 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
print('Средняя продолжительность длительности сессии пользователей: {} секунд или {:.0f} минут'\
.format(round(data['session_duration'].median()), (round(data['session_duration'].median()))/60))
sns.displot(data.query('session_duration > 0')['session_duration'], bins=100, kde=True, height=7, )
plt.title('Распределение длительности сессии пользователя')
plt.xlabel('Длительность сессии в секундах')
plt.ylabel('Частота встречаемости')
plt.grid(which='major')
plt.show()
Средняя продолжительность длительности сессии пользователей: 635 секунд или 11 минут
Как видим средняя продолжительность сессии пользователей составляет 11 минут. В данном случае мы не учитываем длительность сессий равных 0, так как такая длительность сессий может быть если пользователя выкинуло из приложения, произошел сбой, или пользователь случайно открыл и закрыл приложение, не успев совершить в нем действия.
Рассчитаем процентные доли каждого совершаемого события в разрезе каждой из групп: совершивших contacts_show и не совершивших действие contacts_show. Проверим, есть ли закономерность между этими группами.
Для начала создадим две таблицы: в первую занесем пользователей, которые совершали contacts_show, а во вторую - которые не совершали данное действие.
#Создадим таблицу в которой будет user_id и его уникальные события
user_unique_events = pd.DataFrame()
user_unique_events = data.groupby('user_id')['event_name'].unique().reset_index()
user_unique_events
| user_id | event_name | |
|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | [tips_show, map] |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | [search_1, photos_show, favorites_add, contact... |
| 2 | 00463033-5717-4bf1-91b4-09183923b9df | [photos_show] |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | [search_7, search_5, map, search_4, search_6, ... |
| 4 | 00551e79-152e-4441-9cf7-565d7eb04090 | [contacts_show, contacts_call, search_1, photo... |
| ... | ... | ... |
| 4288 | ffab8d8a-30bb-424a-a3ab-0b63ebbf7b07 | [map, tips_show] |
| 4289 | ffc01466-fdb1-4460-ae94-e800f52eb136 | [photos_show, contacts_show] |
| 4290 | ffcf50d9-293c-4254-8243-4890b030b238 | [tips_show, map] |
| 4291 | ffe68f10-e48e-470e-be9b-eeb93128ff1a | [search_1, photos_show, contacts_show] |
| 4292 | fffb9e79-b927-4dbb-9b48-7fd09b23a62b | [tips_show, map, contacts_show] |
4293 rows × 2 columns
# Создадим две таблицы, в одной будут пользователи, совершившие contacts_show, в другой - не совершившие
user_do_contacts_show = pd.DataFrame()
user_no_do_contacts_show = pd.DataFrame()
user_contacts_show_tmp = user_unique_events.copy()
user_contacts_show_tmp['index'] = user_unique_events['event_name']\
.apply(lambda x: np.where(x=="contacts_show")).explode().explode()
user_do_contacts_show = user_contacts_show_tmp.query('index.notnull()')
user_no_do_contacts_show = user_contacts_show_tmp.query('index.isnull()')
#Удаляем промежуточные столбы и добавляем столбцы из оригинальной таблицы data
user_do_contacts_show = user_do_contacts_show.drop(columns=['event_name', 'index'])
user_do_contacts_show = user_do_contacts_show.merge(data, how='left', on='user_id')
user_no_do_contacts_show = user_no_do_contacts_show.drop(columns=['event_name', 'index'])
user_no_do_contacts_show = user_no_do_contacts_show.merge(data, how='left', on='user_id')
print('Кол-во уникальных пользователей, совершивших просмотр контактов', user_do_contacts_show['user_id'].nunique(),\
'\nКол-во уникальныхпользователей, не совершивших просмотр контактов', user_no_do_contacts_show['user_id'].nunique())
Кол-во уникальных пользователей, совершивших просмотр контактов 981 Кол-во уникальныхпользователей, не совершивших просмотр контактов 3312
Если сложить количество уникальных пользователей из этих двух таблиц, то заметим, что оно соответсвует количеству уникальных пользователей в оригинальной таблице data, а именно 4293.
#Вывод таблиц
display(user_do_contacts_show.head().style.set_caption('Таблица с пользователями, совершившими contacts_show'),\
user_no_do_contacts_show.head().style.set_caption('Таблица с пользователями, не совершившими contacts_show'))
| user_id | event_time | event_name | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 0 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:33.849769 | search_1 | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:35:19.296599 | search_1 | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 2 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:36:44.344691 | search_1 | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 3 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:40:38.990477 | photos_show | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:42:13.837523 | photos_show | 2019-10-19 00:00:00 | 5 | 739.000000 |
| user_id | event_time | event_name | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:40:31.052909 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:41:05.722489 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:43:20.735461 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
| 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:45:30.917502 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
У нас получилось 2 таблицы в которых содержатся все действия пользователей, которые совершили действие - просмотр контактов, и которые не совершили действие - просмотр контактов.
def unique_events_df(df, col1, col2):
"""
Функция берет датафрейм и оставляет в нем только те строки,
в которых нет подряд повторяющихся значений в определенных столбцах.
"""
sorted_df = df.loc[(df[[col1, col2]].shift() != df[[col1, col2]]).any(axis=1)]
return sorted_df
user_do_contacts_show_unique_events = unique_events_df(user_do_contacts_show, 'session_id', 'event_name')
user_no_do_contacts_show_unique_events = unique_events_df(user_no_do_contacts_show, 'session_id', 'event_name')
display(user_do_contacts_show_unique_events.head().style\
.set_caption('Таблица с пользователями, совершившими contacts_show и их уникальными событиями'),
user_no_do_contacts_show_unique_events.head().style\
.set_caption('Таблица с пользователями, не совершившими contacts_show и их уникальными событиями'))
| user_id | event_time | event_name | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 0 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:33.849769 | search_1 | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 3 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:40:38.990477 | photos_show | 2019-10-19 00:00:00 | 5 | 739.000000 |
| 7 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:58:00.109019 | search_1 | 2019-10-19 00:00:00 | 6 | 115.000000 |
| 8 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:59:54.637098 | photos_show | 2019-10-19 00:00:00 | 6 | 115.000000 |
| 9 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 18:49:24.115634 | search_1 | 2019-10-20 00:00:00 | 7 | 0.000000 |
| user_id | event_time | event_name | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:45.989359 | tips_show | 2019-10-07 00:00:00 | 1 | 596.000000 |
| 9 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 18:33:55.577963 | map | 2019-10-09 00:00:00 | 2 | 507.000000 |
| 11 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 18:40:28.738785 | tips_show | 2019-10-09 00:00:00 | 2 | 507.000000 |
| 13 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 19:52:30.778932 | tips_show | 2019-10-21 00:00:00 | 3 | 899.000000 |
| 15 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 19:53:38.767230 | map | 2019-10-21 00:00:00 | 3 | 899.000000 |
Создали две таблицы: в одной пользователи, которые совершили событие contacts_show, во второй - не совершившие contacts_show и уникальные события пользователей.
Посчитаем относительные частоты встречаеющихся событий в этих таблицах.
#Функция посчитает долевое соотношение событий в группе
def rel_frequency(df, column_name):
frequency = [(value, df.query('{} == "{}"'.format(column_name, value))\
.count()[column_name] / len(df)) for value in set(df[column_name])]
return frequency
#Посчитаем долевые значения, которые занимают события в группе, совершившей contacts_show
contacts_show_users = rel_frequency(user_do_contacts_show, 'event_name')
rel_contacts_show = []
for rf in contacts_show_users:
rel_contacts_show.append(rf[1])
print("Сумма долевых значений =", sum(rel_contacts_show))
contacts_show_users = pd.DataFrame(list(contacts_show_users))
contacts_show_users.columns = ['event_name', 'part']
contacts_show_users = contacts_show_users.sort_values(by='part', ascending=False)
contacts_show_users
Сумма долевых значений = 1.0
| event_name | part | |
|---|---|---|
| 6 | tips_show | 0.469464 |
| 8 | contacts_show | 0.166526 |
| 9 | photos_show | 0.140751 |
| 7 | advert_open | 0.058426 |
| 14 | search_1 | 0.049307 |
| 2 | map | 0.040482 |
| 3 | contacts_call | 0.019892 |
| 10 | favorites_add | 0.015590 |
| 13 | tips_click | 0.012244 |
| 1 | search_5 | 0.009155 |
| 0 | search_4 | 0.005479 |
| 4 | search_3 | 0.005295 |
| 12 | search_2 | 0.003530 |
| 5 | search_6 | 0.002721 |
| 11 | search_7 | 0.001140 |
#Посчитаем долевые значения, которые занимают события в группе, не совершившей contacts_show
no_contacts_show_users = rel_frequency(user_no_do_contacts_show, 'event_name')
rel_no_contacts_show = []
for rf in no_contacts_show_users:
rel_no_contacts_show.append(rf[1])
print("Сумма долевых значений =", sum(rel_no_contacts_show))
no_contacts_show_users = pd.DataFrame(list(no_contacts_show_users))
no_contacts_show_users.columns = ['event_name', 'part']
no_contacts_show_users = no_contacts_show_users.sort_values(by='part', ascending=False)
no_contacts_show_users
Сумма долевых значений = 1.0
| event_name | part | |
|---|---|---|
| 5 | tips_show | 0.580574 |
| 7 | photos_show | 0.131574 |
| 6 | advert_open | 0.097340 |
| 2 | map | 0.059149 |
| 12 | search_1 | 0.046064 |
| 8 | favorites_add | 0.021128 |
| 1 | search_5 | 0.017021 |
| 0 | search_4 | 0.011745 |
| 11 | tips_click | 0.010234 |
| 3 | search_6 | 0.008213 |
| 4 | search_3 | 0.008043 |
| 10 | search_2 | 0.004851 |
| 9 | search_7 | 0.004064 |
Сформировали две таблицы, которые содержат названия событий, совершенные пользователями и относительную частоту этих событий. Для проверки в обоих случаях убеждаемся, что сумма долей равна 1.
Построим график соотношения этих долей в двух группах.
fig = go.Figure()
fig.add_trace(go.Bar(
x=no_contacts_show_users['part'],
y=no_contacts_show_users['event_name'],
name='Группа не совершала contacts_show',
orientation='h',
marker=dict(
color='rgba(233, 76, 76, 0.8)',
line=dict(color='rgba(181, 0, 0, 0.8)', width=3)
)
))
fig.add_trace(go.Bar(
x=contacts_show_users['part'],
y=contacts_show_users['event_name'],
name='Группа совершала contacts_show',
orientation='h',
marker=dict(
color='rgba(134, 228, 93, 0.8)',
line=dict(color='rgba(53, 158, 8, 0.8)', width=3)
)
))
fig.update_layout(barmode='stack',
width=950,
height=700,
title='Относительная частота событий в разрезе двух групп',
yaxis={'categoryorder':'total ascending'})
fig.show()
Можно заметить, что группа, которая не смотрела контакты contacts_show, чаще открывала карточки объявлений (advert_open), открывала карту объявлений (map), чаще добавляла объявления в избранное (favorites_add), а также чаще видела рекомендованные объявления (tips_show). Группа, смотревшая контакты contacts_show чаще кликала по рекомендованному объявлению (tips_click), чаще просматривала фотографии объявления (photos_show) и делали действие поиска (search_1).
В разрезе сессий отберем популярные сценарии пользователей, которые приводят к совершению целевого действия - просмотр контактов contacts_show. А также построим диаграмму.
Для поиска сценариев до совершения целевого действия будем использовать созданную нами таблицу с пользователями, которые совершили целевое действие - contacts_show.
data_sorted = unique_events_df(data, 'session_id', 'event_name')
data_sorted.head()
| event_time | event_name | user_id | date | session_id | session_duration | |
|---|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 |
| 6541 | 2019-10-09 18:33:55.577963 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 |
| 6565 | 2019-10-09 18:40:28.738785 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 |
| 36412 | 2019-10-21 19:52:30.778932 | tips_show | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 |
| 36419 | 2019-10-21 19:53:38.767230 | map | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 |
Построим диаграмму Сэнкей для отбора популярных сценариев пользователей до совершения ключевого действия contacts_show.
def add_features(df, add_target_2=False):
"""Функция генерации новых столбцов для исходной таблицы
Args:
df (pd.DataFrame): исходная таблица.
Returns:
pd.DataFrame: таблица с новыми признаками.
"""
# сортируем по id и времени
sorted_df = df.sort_values(by=['session_id', 'event_time']).copy()
# добавляем шаги событий
sorted_df['step'] = sorted_df.groupby('session_id').cumcount() + 1
# добавляем узлы-источники и целевые узлы
# узлы-источники - это сами события
sorted_df['source'] = sorted_df['event_name']
# добавляем целевые узлы
sorted_df['target'] = sorted_df.groupby('session_id')['source'].shift(-1)
if add_target_2:
sorted_df['target_2'] = sorted_df.groupby('session_id')['source'].shift(-2)
# возврат таблицы без имени событий
return sorted_df.drop(['event_name'], axis=1)
table = add_features(data_sorted)
table.head()
| event_time | user_id | date | session_id | session_duration | step | source | target | |
|---|---|---|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 | 1 | tips_show | NaN |
| 6541 | 2019-10-09 18:33:55.577963 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 | 1 | map | tips_show |
| 6565 | 2019-10-09 18:40:28.738785 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 | 2 | tips_show | NaN |
| 36412 | 2019-10-21 19:52:30.778932 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 | 1 | tips_show | map |
| 36419 | 2019-10-21 19:53:38.767230 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 | 2 | map | tips_show |
# удалим все пары source-target, шаг которых превышает 7
df_comp = table[table['step'] <= 7].copy().reset_index(drop=True)
def get_source_index(df):
"""Функция генерации индексов source
Args:
df (pd.DataFrame): исходная таблица с признаками step, source, target.
Returns:
dict: словарь с индексами, именами и соответсвиями индексов именам source.
"""
res_dict = {}
count = 0
# получаем индексы источников
for no, step in enumerate(df['step'].unique().tolist()):
# получаем уникальные наименования для шага
res_dict[no+1] = {}
res_dict[no+1]['sources'] = df[df['step'] == step]['source'].unique().tolist()
res_dict[no+1]['sources_index'] = []
for i in range(len(res_dict[no+1]['sources'])):
res_dict[no+1]['sources_index'].append(count)
count += 1
# соединим списки
for key in res_dict:
res_dict[key]['sources_dict'] = {}
for name, no in zip(res_dict[key]['sources'], res_dict[key]['sources_index']):
res_dict[key]['sources_dict'][name] = no
return res_dict
source_indexes = get_source_index(df_comp)
def show_example(step, source_indexes=source_indexes):
"""Функция для вывода данных для конкретного шага будущей диаграммы
Args:
step (int): шаг.
source_indexes (dict): словарь с данными по source на каждом шаге диаграммы
Returns:
"""
print(f'Пример подготовленных данных для шага {step}\n')
for key in source_indexes[step]:
print(f'{key}\n', source_indexes[step][key], '\n')
show_example(3)
Пример подготовленных данных для шага 3
sources
['tips_show', 'map', 'search_1', 'photos_show', 'search_6', 'contacts_call', 'contacts_show', 'advert_open', 'search_4', 'search_5', 'search_3', 'tips_click', 'favorites_add', 'search_2', 'search_7']
sources_index
[29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43]
sources_dict
{'tips_show': 29, 'map': 30, 'search_1': 31, 'photos_show': 32, 'search_6': 33, 'contacts_call': 34, 'contacts_show': 35, 'advert_open': 36, 'search_4': 37, 'search_5': 38, 'search_3': 39, 'tips_click': 40, 'favorites_add': 41, 'search_2': 42, 'search_7': 43}
def colors_for_sources(mode):
"""Генерация цветов rgba
Args:
mode (str): сгенерировать случайные цвета, если 'random', а если 'custom' -
использовать заранее подготовленные
Returns:
dict: словарь с цветами, соответствующими каждому индексу
"""
# словарь, в который сложим цвета в соответствии с индексом
colors_dict = {}
if mode == 'random':
# генерим случайные цвета
for label in df_comp['source'].unique():
r, g, b = np.random.randint(255, size=3)
colors_dict[label] = f'rgba({r}, {g}, {b}, 1)'
elif mode == 'custom':
# присваиваем ранее подготовленные цвета
colors = requests\
.get('https://raw.githubusercontent.com/rusantsovsv/senkey_tutorial/main/json/colors_senkey.json').json()
for no, label in enumerate(df_comp['source'].unique()):
colors_dict[label] = colors['custom_colors'][no]
return colors_dict
colors_dict = colors_for_sources(mode='custom')
# пересчитаем количестов юзеров в процентах от входа
def percent_users(sources, targets, values):
"""
Расчет уникальных id в процентах (для вывода в hover text каждого узла)
Args:
sources (list): список с индексами source.
targets (list): список с индексами target.
values (list): список с "объемами" потоков.
Returns:
list: список с "объемами" потоков в процентах
"""
# объединим источники и метки и найдем пары
zip_lists = list(zip(sources, targets, values))
new_list = []
# подготовим список словарь с общим объемом трафика в узлах
unique_dict = {}
# проходим по каждому узлу
for source, target, value in zip_lists:
if source not in unique_dict:
# находим все источники и считаем общий трафик
unique_dict[source] = 0
for sr, tg, vl in zip_lists:
if sr == source:
unique_dict[source] += vl
# считаем проценты
for source, target, value in zip_lists:
new_list.append(round(100 * value / unique_dict[source], 1))
return new_list
def lists_for_plot(source_indexes=source_indexes, colors=colors_dict, frac=10):
"""
Создаем необходимые для отрисовки диаграммы переменные списков
Args:
source_indexes (dict): словарь с именами и индексами source.
colors (dict): словарь с цветами source.
frac (int): ограничение на минимальный "объем" между узлами.
Returns:
dict: словарь со списками, необходимыми для диаграммы.
"""
sources = []
targets = []
values = []
labels = []
link_color = []
link_text = []
# проходим по каждому шагу
for step in tqdm(sorted(df_comp['step'].unique()), desc='Шаг'):
if step + 1 not in source_indexes:
continue
# получаем индекс источника
temp_dict_source = source_indexes[step]['sources_dict']
# получаем индексы цели
temp_dict_target = source_indexes[step+1]['sources_dict']
# проходим по каждой возможной паре, считаем количество таких пар
for source, index_source in tqdm(temp_dict_source.items()):
for target, index_target in temp_dict_target.items():
# делаем срез данных и считаем количество id
temp_df = df_comp[(df_comp['step'] == step)&(df_comp['source'] == source)&(df_comp['target'] == target)]
value = len(temp_df)
# проверяем минимальный объем потока и добавляем нужные данные
if value > frac:
sources.append(index_source)
targets.append(index_target)
values.append(value)
# делаем поток прозрачным для лучшего отображения
link_color.append(colors[source].replace(', 1)', ', 0.2)'))
labels = []
colors_labels = []
for key in source_indexes:
for name in source_indexes[key]['sources']:
labels.append(name)
colors_labels.append(colors[name])
# посчитаем проценты всех потоков
perc_values = percent_users(sources, targets, values)
# добавим значения процентов для howertext
link_text = []
for perc in perc_values:
link_text.append(f"{perc}%")
# возвратим словарь с вложенными списками
return {'sources': sources,
'targets': targets,
'values': values,
'labels': labels,
'colors_labels': colors_labels,
'link_color': link_color,
'link_text': link_text}
data_for_plot = lists_for_plot()
Шаг: 0%| | 0/7 [00:00<?, ?it/s]
0%| | 0/14 [00:00<?, ?it/s]
0%| | 0/15 [00:00<?, ?it/s]
0%| | 0/15 [00:00<?, ?it/s]
0%| | 0/15 [00:00<?, ?it/s]
0%| | 0/15 [00:00<?, ?it/s]
0%| | 0/15 [00:00<?, ?it/s]
def plot_senkey_diagram(data_dict=data_for_plot):
"""
Функция для генерации объекта диаграммы Сенкей
Args:
data_dict (dict): словарь со списками данных для построения.
Returns:
plotly.graph_objs._figure.Figure: объект изображения.
"""
fig = go.Figure(data=[go.Sankey(
domain = dict(
x = [0,1],
y = [0,1]
),
orientation = "h",
valueformat = ".0f",
node = dict(
pad = 50,
thickness = 15,
line = dict(color = "black", width = 0.1),
label = data_dict['labels'],
color = data_dict['colors_labels']
),
link = dict(
source = data_dict['sources'], # indices correspond to labels, eg A1, A2, A1, B1, ...
target = data_dict['targets'],
value = data_dict['values'],
label = data_dict['link_text'],
color = data_dict['link_color']
))])
fig.update_layout(title_text="Sankey Diagram", font_size=10, width=1000, height=900)
# возвращаем объект диаграммы
return fig
senkey_diagram = plot_senkey_diagram()
senkey_diagram.show()
На диаграмме видно множество сценариев до целевого действия - просмотр контактов, отберем 4 популярных сценария пользователя и построим воронки вовлечения пользователей до целевого действия.
Выберем следующие сценарии: 1) Tips_show -> contacts_show;
2) Photos_show -> contacts_show;
3) Search_1 -> photos_show -> contacts_show;
4) Map -> tips_show -> contacts_show.
Сделаем таблицы, в которых будет отображаться какое количество уникальных пользователей, которые совершают события сценария. Далее построим воронки и посмотрим сколько пользователей прошли от начала сценария до целевого действия contacts_show.
data_sorted_new = add_features(data_sorted, add_target_2=True)
data_sorted_new.head()
| event_time | user_id | date | session_id | session_duration | step | source | target | target_2 | |
|---|---|---|---|---|---|---|---|---|---|
| 805 | 2019-10-07 13:39:45.989359 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 | 1 | 596.0 | 1 | tips_show | NaN | NaN |
| 6541 | 2019-10-09 18:33:55.577963 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 | 1 | map | tips_show | NaN |
| 6565 | 2019-10-09 18:40:28.738785 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 | 2 | 507.0 | 2 | tips_show | NaN | NaN |
| 36412 | 2019-10-21 19:52:30.778932 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 | 1 | tips_show | map | tips_show |
| 36419 | 2019-10-21 19:53:38.767230 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 | 3 | 899.0 | 2 | map | tips_show | map |
def funnel_scenario_table(df, funnel_dict, funnel_by):
"""
Функция принимает датафрейм, словарь в котором указан сценарий и название столбца по которому
необходимо делать подсчет.
Возвращает датафрейм в котором 2 столбца: наименование события и количество уникальных пользователей,
совершивших это событие.
"""
funnel_columns = ['name', 'result', 'part']
funnel_names = []
funnel_results = []
query = '{} == "{}"'
for key in funnel_dict:
query = query.format(key, funnel_dict[key])
funnel_names.append(funnel_dict[key])
funnel_results.append(df.query(query)[funnel_by].nunique())
query += ' and {} == "{}"'
data = {
'name': funnel_names,
'result': funnel_results
}
funnel_df = pd.DataFrame().from_dict(data)
return funnel_df
def funnel_graphic(y, x):
"""
функция строит график воронку событий, количества пользователей
и процент пользователей перешедших с одного шага на другой
"""
fig = go.Figure(go.Funnel(y=y,
x=x,
textposition='inside',
textinfo='value + percent previous',
textfont_size=14,
marker={"color":['#ed7953', '#fb9f3a', '#fdca26', '#f0f921']}
))
fig.update_layout(
title={'text':'Воронка событий',
'y':0.9,
'x':0.55,
'xanchor':'center',
'yanchor':'top'})
fig.show()
# Tips_show -> contacts_show;
scenario_1 = funnel_scenario_table(data_sorted_new , {'source': 'tips_show', 'target': 'contacts_show'}, 'user_id')
scenario_1.columns = ['Наименование события', 'Количество событий']
scenario_1
| Наименование события | Количество событий | |
|---|---|---|
| 0 | tips_show | 2801 |
| 1 | contacts_show | 434 |
funnel_graphic(scenario_1['Наименование события'], scenario_1['Количество событий'])
Из всего количества уникальных пользователей совершивших действие tips_show на шаг с событием contacts_show перешло всего 15%.
#Photos_show -> contacts_show
scenario_2 = funnel_scenario_table(
data_sorted_new ,
{'source': 'photos_show', 'target': 'contacts_show'},
'user_id'
)
scenario_2.columns = ['Наименование события', 'Количество событий']
scenario_2
| Наименование события | Количество событий | |
|---|---|---|
| 0 | photos_show | 1095 |
| 1 | contacts_show | 188 |
funnel_graphic(scenario_2['Наименование события'], scenario_2['Количество событий'])
Из общего количества пользователей, двигающихся по 2 сценарию, после действия photos_show действие contacts_show совершило 17%.
# Search_1 -> photos_show -> contacts_show
scenario_3 = funnel_scenario_table(
data_sorted_new ,
{'source': 'search_1', 'target': 'photos_show', 'target_2': 'contacts_show'},
'user_id'
)
scenario_3.columns = ['Наименование события', 'Количество событий']
scenario_3
| Наименование события | Количество событий | |
|---|---|---|
| 0 | search_1 | 787 |
| 1 | photos_show | 466 |
| 2 | contacts_show | 55 |
funnel_graphic(scenario_3['Наименование события'], scenario_3['Количество событий'])
Из 787 пользователей, совершивших событие search-1 только 12% дошло до целевого действия.
# Map -> tips_show -> contacts_show.
scenario_4 = funnel_scenario_table(
data_sorted_new,
{'source': 'map', 'target': 'tips_show', 'target_2': 'contacts_show'},
'user_id'
)
scenario_4.columns = ['Наименование события', 'Количество событий']
scenario_4
| Наименование события | Количество событий | |
|---|---|---|
| 0 | map | 1456 |
| 1 | tips_show | 937 |
| 2 | contacts_show | 99 |
funnel_graphic(scenario_4['Наименование события'], scenario_4['Количество событий'])
Из 1456 пользователей совершивших событие map до целевого действия contacts_show дошло 11% пользователей.
Среди всех сценариев наиболее благоприятным является сценарий photos_show -> contacts_show по нему с первого шага до целевого шага перешел больший процент пользователей: 17%.
Для проверки гипотезы необходимо будет сравнить конверсии по каждому событию в каждой из двух групп (в группе пользователей совершивших tips_show и tips_click и группе, которая совершила только tips_show), узнать отличаются ли эти доли или же мы можем утверждать, что между ними нет разницы.
Для этого сформируем нулевую и альтернативную гипотезы:
Для начала сформируем таблицу с двумя группами. В первую группу будут входить пользователи, которые совершили и tips_show и tips_click, а во втоорую будут входить пользователи, которые совершили tips_show и не совершали tips_click.
def hypo_groups_df(df, col1, col2):
"""
Функция создает два датафрейма, в первом - пользователи, которые совершили оба действия, которые передаем в функцию
Второй датафрейм содержит информацию о пользователях, которые совершили первое действие, но не совершили второе
"""
pd.options.mode.chained_assignment = None
temp = pd.DataFrame()
temp = df.copy()
temp = temp.groupby('user_id')['event_name'].unique().reset_index()
#Отберем всех пользователей которые совершали col1
temp['index'] = temp['event_name'].apply(lambda x: np.where(x==col1)).explode().explode()
temp = temp.query('index.notnull()')
#Разделим этих пользователей на тех кто также совершал col1 и тех, кто не совершал col2
temp['index_2'] = temp['event_name'].apply(lambda x: np.where(x==col2)).explode().explode()
#group_first будет содержать пользователей, которые сделали и col1 и favorites_add
#group_second будет содержать пользователей, которые сделали col1 и не сделали col2
group_first = temp.query('index_2.notnull()')
group_second = temp.query('index_2.isnull()')
#Удаляем промежуточные столбы и добавляем столбцы из оригинальной таблицы data
group_first = group_first.drop(columns=['event_name', 'index', 'index_2'])
group_first = group_first.merge(df, how='left', on='user_id')
group_second = group_second.drop(columns=['event_name', 'index', 'index_2'])
group_second = group_second.merge(df, how='left', on='user_id')
# #Добавляем в таблицы столбцы source и target
g_first = add_features(group_first, add_target_2=True)
g_second = add_features(group_second, add_target_2=True)
display(g_first.head().style.set_caption("Таблица пользователей, которые сделали и {} и {}".format(col1, col2)),\
g_second.head().style.set_caption\
("Таблица пользователей, которые сделали {} и не сделали {}".format(col1, col2)))
return g_first, g_second
group_one, group_two = hypo_groups_df(data_sorted, "tips_show", "tips_click")
| user_id | event_time | date | session_id | session_duration | step | source | target | target_2 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 01147bf8-cd48-49c0-a5af-3f6eb45f8262 | 2019-11-01 21:13:53.377582 | 2019-11-01 00:00:00 | 59 | 4148.000000 | 1 | tips_show | tips_click | tips_show |
| 1 | 01147bf8-cd48-49c0-a5af-3f6eb45f8262 | 2019-11-01 22:20:20.323377 | 2019-11-01 00:00:00 | 59 | 4148.000000 | 2 | tips_click | tips_show | tips_click |
| 2 | 01147bf8-cd48-49c0-a5af-3f6eb45f8262 | 2019-11-01 22:20:25.502309 | 2019-11-01 00:00:00 | 59 | 4148.000000 | 3 | tips_show | tips_click | tips_show |
| 3 | 01147bf8-cd48-49c0-a5af-3f6eb45f8262 | 2019-11-01 22:22:56.152742 | 2019-11-01 00:00:00 | 59 | 4148.000000 | 4 | tips_click | tips_show | nan |
| 4 | 01147bf8-cd48-49c0-a5af-3f6eb45f8262 | 2019-11-01 22:23:01.365577 | 2019-11-01 00:00:00 | 59 | 4148.000000 | 5 | tips_show | nan | nan |
| user_id | event_time | date | session_id | session_duration | step | source | target | target_2 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-07 13:39:45.989359 | 2019-10-07 00:00:00 | 1 | 596.000000 | 1 | tips_show | nan | nan |
| 1 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 18:33:55.577963 | 2019-10-09 00:00:00 | 2 | 507.000000 | 1 | map | tips_show | nan |
| 2 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-09 18:40:28.738785 | 2019-10-09 00:00:00 | 2 | 507.000000 | 2 | tips_show | nan | nan |
| 3 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 19:52:30.778932 | 2019-10-21 00:00:00 | 3 | 899.000000 | 1 | tips_show | map | tips_show |
| 4 | 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 | 2019-10-21 19:53:38.767230 | 2019-10-21 00:00:00 | 3 | 899.000000 | 2 | map | tips_show | map |
group_one = funnel_scenario_table(
group_one,
{'source': 'tips_show', 'target': 'tips_click', 'target_2': 'contacts_show'},
'user_id'
)
group_one
| name | result | |
|---|---|---|
| 0 | tips_show | 297 |
| 1 | tips_click | 267 |
| 2 | contacts_show | 2 |
#Формируем иаблицу с пользователями, которые совершили действие tips_show и не совершили tips_click
group_two = funnel_scenario_table(
group_two,
{'source': 'tips_show', 'target': 'contacts_show'},
'user_id'
)
group_two
| name | result | |
|---|---|---|
| 0 | tips_show | 2504 |
| 1 | contacts_show | 358 |
def check_hypothesis(g1, g2):
"""
Функция проверяет гипотезы
"""
#Уровень статистической значимости
alpha = 0.05
#Число пользователей совершивших события по группам
successes = np.array([g1.iloc[len(g1) - 1]['result'],
g2.iloc[len(g2) - 1]['result']])
#Общее кол-во пользователей в группах
number_of_users = np.array([g1.iloc[0]['result'],
g2.iloc[0]['result']])
print(successes, number_of_users) # КОД РЕВЬЮЕРА
#Пропорции успехов в 1 и 2 группах
p1 = successes[0] / number_of_users[0]
p2 = successes[1] / number_of_users[1]
#Пропорция успехов в комбинированном датафрейме
p_combined = (successes[0] + successes[1]) / (number_of_users[0] + number_of_users[1])
#Разница пропорций
difference = p1 - p2
#Считаем статистику в ст. отклонениях стандартного норм. распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) *\
(1 / number_of_users[0] + 1 / number_of_users[1]))
#Задаем стандартное нормальное распределение
distr = st.norm(0, 1)
#Если бы пропорции были равны, разница между ними = 0, т.к. распределение норм., вызовем метод
#cdf(), модуль abs() т.к. тест двусторонний, по этой же причине удваиваем результат
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение:', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между конверсиями есть разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать конверсии разными')
check_hypothesis(group_one, group_two)
[ 2 358] [ 297 2504] p-значение: 3.288413985558236e-11 Отвергаем нулевую гипотезу: между конверсиями есть разница
Значение p-value достаточно маленькое и маловероятно, что наблюдаемые закономерности между группами - результат случайных процессов, значит, конверсия в просмотры контактов у группы пользователей, которые совершают действия tips_show и tips_click и группы, которая совершает только tips_show и не совершает tips_click разная.
Так как большое количество пользователей совершает advert_open, хочется проверить, как просмотр объявления связан с другими событиями и влияет ли это на конверсию. Поэтому проверим гипотезу о том, что конверсия в просмотры контактов различается у группы пользователей, которые совершили advert_open и photos_show и группы, которая совершила только advert_open.
Для этого сформируем нулевую и альтернативную гипотезы:
Сформируем таблицу с двумя группами. В первую группу будут входить пользователи, которые совершили и advert_open и photos_show, а во втоорую будут входить пользователи, которые совершили advert_open и не совершали photos_show.
group_ones, group_twos = hypo_groups_df(data_sorted, "advert_open", "photos_show")
| user_id | event_time | date | session_id | session_duration | step | source | target | target_2 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:34:33.849769 | 2019-10-19 00:00:00 | 5 | 739.000000 | 1 | search_1 | photos_show | nan |
| 1 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:40:38.990477 | 2019-10-19 00:00:00 | 5 | 739.000000 | 2 | photos_show | nan | nan |
| 2 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:58:00.109019 | 2019-10-19 00:00:00 | 6 | 115.000000 | 1 | search_1 | photos_show | nan |
| 3 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-19 21:59:54.637098 | 2019-10-19 00:00:00 | 6 | 115.000000 | 2 | photos_show | nan | nan |
| 4 | 00157779-810c-4498-9e05-a1e9e3cedf93 | 2019-10-20 18:49:24.115634 | 2019-10-20 00:00:00 | 7 | 0.000000 | 1 | search_1 | nan | nan |
| user_id | event_time | date | session_id | session_duration | step | source | target | target_2 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:14:05.555052 | 2019-10-18 00:00:00 | 20 | 215.000000 | 1 | search_7 | search_5 | map |
| 1 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:14:16.960831 | 2019-10-18 00:00:00 | 20 | 215.000000 | 2 | search_5 | map | nan |
| 2 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-18 22:17:40.719687 | 2019-10-18 00:00:00 | 20 | 215.000000 | 3 | map | nan | nan |
| 3 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-20 17:47:18.569612 | 2019-10-20 00:00:00 | 21 | 84.000000 | 1 | search_7 | search_4 | search_6 |
| 4 | 004690c3-5a84-4bb7-a8af-e0c8f8fca64e | 2019-10-20 17:47:19.889629 | 2019-10-20 00:00:00 | 21 | 84.000000 | 2 | search_4 | search_6 | search_5 |
#Формируем таблицу с пользователями, которые совершили действия advert_open и favorites_add
group_ones = funnel_scenario_table(
group_ones,
{'source': 'advert_open', 'target': 'photos_show', 'target_2': 'contacts_show'},
'user_id'
)
group_ones
| name | result | |
|---|---|---|
| 0 | advert_open | 73 |
| 1 | photos_show | 52 |
| 2 | contacts_show | 5 |
#Формируем таблицу с пользователями, которые совершили действия advert_open
group_twos = funnel_scenario_table(
group_twos,
{'source': 'advert_open', 'target': 'contacts_show'},
'user_id'
)
group_twos
| name | result | |
|---|---|---|
| 0 | advert_open | 678 |
| 1 | contacts_show | 25 |
check_hypothesis(group_ones, group_twos)
[ 5 25] [ 73 678] p-значение: 0.1899321346713685 Не получилось отвергнуть нулевую гипотезу, нет оснований считать конверсии разными
p-value получилось больше статистической значимости, значит, можем счтитать, что конверсия у двух групп, которые совершали просмотр карточки объявления и просмотр фото, и которые совершали только просмотр карточки объявления, одинаковая.
В данном проекте было проанализированно поведение пользователей приложения "Ненужные вещи". Были отобраны популярные сценарии, по которым двигаются пользователи до достижения целевого действия, а также построены воронки с конверсией пользователей на каждом шаге сценариев. Была расчитана относительная частота событий в разрезе двух групп: которые смотрели контакты и которые не смотрели контакты.
Была проведена предобработка данных, изменены названия столбцов и типы данных, была проведена проверка дубликатов, было выяснено за какой период представлены данные в датасете: в период с 2019-10-03 по 2019-11-07.
Было выяснено, в приложении 4293 уникальных пользователя и 15 событий. Также было найдено время суток, в которое пользователи приложения наиболее активны: с 14 до 16 часов дня, а также с 20 до 21 вечера.
Было выяснено, что событие, которое совершают уникальные пользователи чаще всего - tips_show, а также событие map и photos_show. Именно эти три события совершает больший процент уникальных пользователей приложения. Если смотреть на количество совершаемых событий в целом, а не только по уникальным пользователям, то самыми популярными событиями являются: tips_show - просмотрели 40055 раз, событие photos_show - 10012 раз, событие advert_open - 6164 раз.
Была найдена средняя длительность сессии пользователей - 11 минут.
Была найдена относительная частота событий в разрезе двух групп пользователей: которые совершали целевое действие просмотр контактов contacts_show и те, которые не совершали целевое событие contacts_show. Было замечено, что группа, которая не смотрела контакты contacts_show, чаще открывала карточки объявлений (advert_open), открывала карту объявлений (map), чаще добавляла объявления в избранное (favorites_add), а также чаще видела рекомендованные объявления (tips_show). Группа, смотревшая контакты contacts_show чаще кликала по рекомендованному объявлению (tips_click), чаще просматривала фотографии объявления (photos_show).
Были найдены наиболее популярные события пользователей в целевое действие - просмотр контактов. Такими сценариями оказались:
Самая большая конверсия в целевое действие оказалась у сценария photos_show -> contacts_show, из 1095 пользователей в целевое действие перешло 17% из них.
Была проведена проверка гипотез:
При проверке первой гипотезы было выяснено, что конверсия в просмотры контактов у двух групп действительно различается. При проверке второй гипотезы было выяснено, что конверсия в целевое действие у двух групп одинаковая.
Рекомендации:
Для улучшения взаимодействия пользователей с приложением рекомендуется улучшить те области приложения, с которым пользователь взаимодействует чаще всего. Например, большое количество уникальных (1456, более 33%) пользователей пользуются map (просмотр карты), также большое количество пользователей, а именно 2801 уникальный пользователь, что составляет более 65% от всех уникальных пользователей, просматривают tips_show (рекомендованные объявления). Так как один из популярных сценариев в целевое действие: map -> tips_show -> contacts_show, то можно персонализировать объявления высвечивая клиенту рекомендованное объявление tips_show исходя из локаций которые он просматривает на карте. Таким образом, можно привлечь внимание пользователя к рекомендованным объявлениям в его регионе.
Также учитывая популярный сценарий: search_1 ->photos_show -> contacts_show можно расширить возможности поиска: добавить дополнительные фильтры, высвечивать сначала те объявления, которые содержат фотографии, так как конверсия в целевое действие выше, когда пользователь просматривает фото. Можно также рекомендовать пользователям добавлять больше фотографий в объявление, аргументируя тем, что это может повысить просмотры их объявлений.
Так как при проверке гипотез мы выяснили, что конверсия в целевое действие идентичная, если пользователь просматривает объявление advert_open и потом просматривает фото photos_show, и если пользователь только просматривает объявление advert_open. Тогда можно добавить дополнительные функции для продвижения объявлений таким образом, чтобы пользователь хотел просмотреть данное объявление, так как конверсия в целевое действие у них тоже будет высокой. Например, приоритизировать локацию объявления или рекомендовать эти объявления пользователю: "возможно, вам интересно...".
Также все вышеописанные действия можно адаптировать под время активности пользователя. Например, когда активность пользователей выше, вероятность того, что они увидят рекомендованные объявления тоже выше, поэтому возможно высвечивать пользователям рекомендации преимущественно с 14 до 16 дня и с 20 до 21 вечера. Это время приходится на обеденные перерывы и вечернее времяприпровождение пользователей. Добавлять нововведения в приложение можно ночью, когда активность пользователей наиболее низкая, а именно с 3 до 6 утра, чтобы не столкнуться с большим негативным опытом пользователей приложения при происхождении неполадок.
Выше описанные действия помогут нам вести пользователей по благоприятным сценариям, а также подкреплять позитивный опыт пользователей приложения, ведь улучшения затронут те области приложения, которыми пользуется большинство пользователей приложения и пользуются ими чаще всего.
Ссылка на презентацию:
https://github.com/leryash/graduation_project/blob/main/presentation.pdf